<!DOCTYPE html>
<!--
Copyright 2015 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->

<link rel="import" href="/tracing/base/color_scheme.html">
<link rel="import" href="/tracing/base/multi_dimensional_view.html">
<link rel="import" href="/tracing/base/scalar.html">
<link rel="import" href="/tracing/base/utils.html">
<link rel="import" href="/tracing/ui/analysis/memory_dump_heap_details_breakdown_view.html">
<link rel="import" href="/tracing/ui/analysis/memory_dump_heap_details_path_view.html">
<link rel="import" href="/tracing/ui/analysis/memory_dump_heap_details_util.html">
<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html">
<link rel="import" href="/tracing/ui/analysis/stacked_pane.html">
<link rel="import" href="/tracing/ui/base/dom_helpers.html">
<link rel="import" href="/tracing/ui/base/drag_handle.html">
<link rel="import" href="/tracing/ui/base/info_bar.html">

<dom-module id='tr-ui-a-memory-dump-heap-details-pane'>
  <template>
    <style>
      :host {
        display: flex;
        flex-direction: column;
      }

      #header {
        flex: 0 0 auto;
        display: flex;
        flex-direction: row;
        align-items: center;

        background-color: #eee;
        border-bottom: 1px solid #8e8e8e;
        border-top: 1px solid white;
      }

      #label {
        flex: 1 1 auto;
        padding: 8px;
        font-size: 15px;
        font-weight: bold;
      }

      #view_mode_container {
        display: none;
        flex: 0 0 auto;
        padding: 5px;
        font-size: 15px;
      }

      #contents {
        flex: 1 0 auto;
        align-self: stretch;
        font-size: 12px;
      }

      #info_text {
        padding: 8px;
        color: #666;
        font-style: italic;
        text-align: center;
      }

      #split_view {
        display: none;  /* Hide until memory allocator dumps are set. */
        flex: 1 0 auto;
        align-self: stretch;
        flex-direction: row;
      }

      #path_view {
        width: 50%;
      }

      #breakdown_view {
        flex: 1 1 auto;
        width: 0;
      }

      #path_view, #breakdown_view {
        overflow-x: auto;  /* Show scrollbar if necessary. */
      }
    </style>
    <div id="header">
      <div id="label">Heap details</div>
      <div id="view_mode_container">
        <span>View mode:</span>
        <!-- View mode selector (added in Polymer.ready()) -->
      </div>
    </div>
    <div id="contents">
      <tr-ui-b-info-bar id="info_bar" hidden>
      </tr-ui-b-info-bar>

      <div id="info_text">No heap dump selected</div>

      <div id="split_view">
        <tr-ui-a-memory-dump-heap-details-path-view id="path_view">
        </tr-ui-a-memory-dump-heap-details-path-view>
        <tr-ui-b-drag-handle id="drag_handle"></tr-ui-b-drag-handle>
        <tr-ui-a-memory-dump-heap-details-breakdown-view id="breakdown_view">
        </tr-ui-a-memory-dump-heap-details-breakdown-view>
      </div>
    </div>
  </template>
</dom-module>
<script>
'use strict';

tr.exportTo('tr.ui.analysis', function() {
  const Scalar = tr.b.Scalar;
  const sizeInBytes_smallerIsBetter =
      tr.b.Unit.byName.sizeInBytes_smallerIsBetter;
  const count_smallerIsBetter = tr.b.Unit.byName.count_smallerIsBetter;
  const MultiDimensionalViewBuilder = tr.b.MultiDimensionalViewBuilder;
  const TotalState = tr.b.MultiDimensionalViewNode.TotalState;

  /** @{constructor} */
  function HeapDumpTreeNode(
      stackFrameNodes, dimension, title, heavyView, parentNode) {
    this.dimension = dimension;
    this.title = title;
    this.parentNode = parentNode;

    this.heavyView_ = heavyView;
    this.stackFrameNodes_ = stackFrameNodes;
    this.lazyCells_ = undefined;
    this.lazyChildNodes_ = undefined;
  }

  HeapDumpTreeNode.prototype = {
    get minDisplayedTotalState_() {
      if (this.heavyView_) {
        // Show lower-bound and exact values in heavy views.
        return TotalState.LOWER_BOUND;
      }
      // Show only exact values in tree view.
      return TotalState.EXACT;
    },

    get childNodes() {
      if (!this.lazyChildNodes_) {
        this.lazyChildNodes_ = new Map();
        this.addDimensionChildNodes_(
            tr.ui.analysis.HeapDetailsRowDimension.STACK_FRAME, 0);
        this.addDimensionChildNodes_(
            tr.ui.analysis.HeapDetailsRowDimension.OBJECT_TYPE, 1);
        this.releaseStackFrameNodesIfPossible_();
      }
      return this.lazyChildNodes_;
    },

    get cells() {
      if (!this.lazyCells_) {
        this.addCells_();
        this.releaseStackFrameNodesIfPossible_();
      }
      return this.lazyCells_;
    },

    releaseStackFrameNodesIfPossible_() {
      if (this.lazyCells_ && this.lazyChildNodes_) {
        // Don't unnecessarily hold a reference to the stack frame nodes when
        // we don't need them anymore.
        this.stackFrameNodes_ = undefined;
      }
    },

    addDimensionChildNodes_(dimension, dimensionIndex) {
      // Child title -> Timestamp (list index) -> Child
      // MultiDimensionalViewNode.
      const dimensionChildTitleToStackFrameNodes = tr.b.invertArrayOfDicts(
          this.stackFrameNodes_,
          node => this.convertStackFrameNodeDimensionToChildDict_(
              node, dimensionIndex));

      // Child title (list index) -> Child HeapDumpTreeNode.
      const dimensionChildNodes = [];
      for (const [childTitle, childStackFrameNodes] of
        Object.entries(dimensionChildTitleToStackFrameNodes)) {
        dimensionChildNodes.push(new HeapDumpTreeNode(childStackFrameNodes,
            dimension, childTitle, this.heavyView_, this));
      }
      this.lazyChildNodes_.set(dimension, dimensionChildNodes);
    },

    convertStackFrameNodeDimensionToChildDict_(
        stackFrameNode, dimensionIndex) {
      const childDict = {};
      let displayedChildrenTotalSize = 0;
      let displayedChildrenTotalCount = 0;
      let hasDisplayedChildren = false;
      let allDisplayedChildrenHaveDisplayedCounts = true;
      for (const child of stackFrameNode.children[dimensionIndex].values()) {
        if (child.values[0].totalState < this.minDisplayedTotalState_) {
          continue;
        }
        if (child.values[1].totalState < this.minDisplayedTotalState_) {
          allDisplayedChildrenHaveDisplayedCounts = false;
        }
        childDict[child.title[dimensionIndex]] = child;
        displayedChildrenTotalSize += child.values[0].total;
        displayedChildrenTotalCount += child.values[1].total;
        hasDisplayedChildren = true;
      }

      const nodeTotalSize = stackFrameNode.values[0].total;
      const nodeTotalCount = stackFrameNode.values[1].total;

      // Add '<other>' node if necessary in tree-view.
      const hasUnclassifiedSizeOrCount =
          displayedChildrenTotalSize < nodeTotalSize ||
          displayedChildrenTotalCount < nodeTotalCount;
      if (!this.heavyView_ && hasUnclassifiedSizeOrCount &&
          hasDisplayedChildren) {
        const otherTitle = stackFrameNode.title.slice();
        otherTitle[dimensionIndex] = '<other>';
        const otherNode = new tr.b.MultiDimensionalViewNode(otherTitle, 2);
        childDict[otherTitle[dimensionIndex]] = otherNode;

        // '<other>' node size.
        otherNode.values[0].total = nodeTotalSize - displayedChildrenTotalSize;
        otherNode.values[0].totalState = this.minDisplayedTotalState_;

        // '<other>' node allocation count.
        otherNode.values[1].total =
            nodeTotalCount - displayedChildrenTotalCount;
        // Don't show allocation count of the '<other>' node if there is a
        // displayed child node that did NOT display allocation count.
        otherNode.values[1].totalState =
            allDisplayedChildrenHaveDisplayedCounts ?
              this.minDisplayedTotalState_ : TotalState.NOT_PROVIDED;
      }

      return childDict;
    },

    addCells_() {
      // Transform a chronological list of heap stack frame tree nodes into a
      // dictionary of cells (where each cell contains a chronological list
      // of the values of its numeric).
      this.lazyCells_ = tr.ui.analysis.createCells(this.stackFrameNodes_,
          function(stackFrameNode) {
            const size = stackFrameNode.values[0].total;
            const numerics = {
              'Size': new Scalar(sizeInBytes_smallerIsBetter, size)
            };
            const countValue = stackFrameNode.values[1];
            if (countValue.totalState >= this.minDisplayedTotalState_) {
              const count = countValue.total;
              numerics.Count = new Scalar(
                  count_smallerIsBetter, count);
            }
            return numerics;
          }, this);
    }
  };

  Polymer({
    is: 'tr-ui-a-memory-dump-heap-details-pane',
    behaviors: [tr.ui.analysis.StackedPane],

    created() {
      this.heapDumps_ = undefined;
      this.viewMode_ = undefined;
      this.aggregationMode_ = undefined;
      this.cachedBuilders_ = new Map();
    },

    ready() {
      this.$.info_bar.message = 'Note: Values displayed in the heavy view ' +
          'are lower bounds (except for the root).';

      Polymer.dom(this.$.view_mode_container).appendChild(
          tr.ui.b.createSelector(
              this, 'viewMode', 'memoryDumpHeapDetailsPane.viewMode',
              MultiDimensionalViewBuilder.ViewType.TOP_DOWN_TREE_VIEW,
              [
                {
                  label: 'Top-down (Tree)',
                  value: MultiDimensionalViewBuilder.ViewType.TOP_DOWN_TREE_VIEW
                },
                {
                  label: 'Top-down (Heavy)',
                  value:
                      MultiDimensionalViewBuilder.ViewType.TOP_DOWN_HEAVY_VIEW
                },
                {
                  label: 'Bottom-up (Heavy)',
                  value:
                      MultiDimensionalViewBuilder.ViewType.BOTTOM_UP_HEAVY_VIEW
                }
              ]));

      this.$.drag_handle.target = this.$.path_view;
      this.$.drag_handle.horizontal = false;

      // If the user selects a node in the path view, show its children in the
      // breakdown view.
      this.$.path_view.addEventListener('selected-node-changed', (function(e) {
        this.$.breakdown_view.displayedNode = this.$.path_view.selectedNode;
      }).bind(this));

      // If the user double-clicks on a node in the breakdown view, select the
      // node in the path view.
      this.$.breakdown_view.addEventListener('enter-node', (function(e) {
        this.$.path_view.selectedNode = e.node;
      }).bind(this));
    },

    /**
     * Sets the heap dumps and schedules rebuilding the pane.
     *
     * The provided value should be a chronological list of heap dumps. All
     * dumps are assumed to belong to the same process and belong to the same
     * allocator. Example:
     *
     *   [
     *     tr.model.HeapDump {},  // Heap dump at timestamp 1.
     *     undefined,  // Heap dump not provided at timestamp 2.
     *     tr.model.HeapDump {},  // Heap dump at timestamp 3.
     *   ]
     */
    set heapDumps(heapDumps) {
      this.heapDumps_ = heapDumps;
      this.scheduleRebuild_();
    },

    get heapDumps() {
      return this.heapDumps_;
    },

    set aggregationMode(aggregationMode) {
      this.aggregationMode_ = aggregationMode;
      this.$.path_view.aggregationMode = aggregationMode;
      this.$.breakdown_view.aggregationMode = aggregationMode;
    },

    get aggregationMode() {
      return this.aggregationMode_;
    },

    set viewMode(viewMode) {
      this.viewMode_ = viewMode;
      this.scheduleRebuild_();
    },

    get viewMode() {
      return this.viewMode_;
    },

    get heavyView() {
      switch (this.viewMode) {
        case MultiDimensionalViewBuilder.ViewType.TOP_DOWN_HEAVY_VIEW:
        case MultiDimensionalViewBuilder.ViewType.BOTTOM_UP_HEAVY_VIEW:
          return true;
        default:
          return false;
      }
    },

    onRebuild_() {
      if (this.heapDumps_ === undefined ||
          this.heapDumps_.length === 0) {
        // Show the info text (hide the table and the view mode selector).
        this.$.info_text.style.display = 'block';
        this.$.split_view.style.display = 'none';
        this.$.view_mode_container.style.display = 'none';
        this.$.info_bar.hidden = true;
        this.$.path_view.selectedNode = undefined;
        return;
      }

      // Show the table and the view mode selector (hide the info text).
      this.$.info_text.style.display = 'none';
      this.$.split_view.style.display = 'flex';
      this.$.view_mode_container.style.display = 'block';

      // Show the info bar if in heavy view mode.
      this.$.info_bar.hidden = !this.heavyView;

      this.$.path_view.selectedNode = this.createHeapTree_();
      this.$.path_view.rebuild();
      this.$.breakdown_view.rebuild();
    },

    createHeapTree_() {
      const definedHeapDump = this.heapDumps_.find(x => x);
      if (definedHeapDump === undefined) return undefined;

      // The title of the root node is the name of the allocator.
      const rootRowTitle = definedHeapDump.allocatorName;

      const stackFrameTrees = this.createStackFrameTrees_(this.heapDumps_);

      return new HeapDumpTreeNode(stackFrameTrees,
          tr.ui.analysis.HeapDetailsRowDimension.ROOT, rootRowTitle,
          this.heavyView);
    },

    createStackFrameTrees_(heapDumps) {
      const builders = heapDumps.map(heapDump => this.createBuilder_(heapDump));
      const views = builders.map(builder => {
        if (builder === undefined) return undefined;
        return builder.buildView(this.viewMode);
      });
      return views;
    },

    createBuilder_(heapDump) {
      if (heapDump === undefined) return undefined;

      if (this.cachedBuilders_.has(heapDump)) {
        return this.cachedBuilders_.get(heapDump);
      }

      const dimensions = 2; // stack frames, object type
      const valueCount = 2; // size, count
      const builder = new MultiDimensionalViewBuilder(dimensions, valueCount);

      // Build the heap tree.
      for (const entry of heapDump.entries) {
        const leafStackFrame = entry.leafStackFrame;
        const stackTracePath = leafStackFrame === undefined ?
          [] : leafStackFrame.getUserFriendlyStackTrace().reverse();

        const objectTypeName = entry.objectTypeName;
        const objectTypeNamePath = objectTypeName === undefined ?
          [] : [objectTypeName];

        const valueKind = entry.valuesAreTotals ?
          MultiDimensionalViewBuilder.ValueKind.TOTAL :
          MultiDimensionalViewBuilder.ValueKind.SELF;

        builder.addPath([stackTracePath, objectTypeNamePath],
            [entry.size, entry.count],
            valueKind);
      }

      builder.complete = heapDump.isComplete;
      this.cachedBuilders_.set(heapDump, builder);
      return builder;
    },
  });

  return {};
});
</script>
